22 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
abb080b065 Updates plugin version to 0.4.0-ALPHA 2022-03-02 00:41:06 +01:00
9bb234169d Adds various improvements, fixes and a new feature
Adds and option to match any paid sign condition instead of all conditions
Adds checks for whether line indices are outside the allowed range
Disallows any invalid regular expressions in sign conditions
2022-03-02 00:37:00 +01:00
664115b2b4 Translates the remaining strings and fixes a few minor bugs 2022-03-01 18:45:53 +01:00
4602ca71db Translates the list command's messages 2022-03-01 16:56:12 +01:00
30bca7b8f1 Translates the add condition command's messages 2022-03-01 16:56:02 +01:00
81e62eb664 Translates the add command's messages 2022-03-01 16:55:50 +01:00
5cbde82fff Adds translations for a bunch of messages 2022-03-01 16:55:30 +01:00
eb0e06f193 Adds code to be able to translate plugin messages 2022-03-01 16:03:08 +01:00
483ffaec2b Makes sure signs are no longer tracked if they no longer exist 2022-02-28 15:35:58 +01:00
797020aa7f Updates version to 0.3.0-ALPHA 2022-02-28 15:14:59 +01:00
15426a46f3 Registers the block break listener and adds some small improvements 2022-02-28 15:06:58 +01:00
a1b1a5d112 Adds sign tracking and refunds #3 #8 2022-02-28 14:41:14 +01:00
30 changed files with 1727 additions and 279 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:
@ -13,10 +16,11 @@ As this plugin only listens to sign change events, there are some limitations:
## Commands
* /addpaidsign <name> <cost> \[permission] \[ignore case] \[ignore color]
* /addpaidsign <name> <cost> \[permission] \[ignore case] \[ignore color] \[match any condition]
* /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
@ -35,6 +39,8 @@ This command adds a new paid sign that does nothing until a condition is added.
default uses the config file value).
* ignore color - Whether any condition of the paid sign should ignore color by default, when matching against text (
default uses the config file value).
* match any condition - Whether to trigger a paid sign match if a single one of the sign's conditions is true. This is
mainly useful if several lines may contain the match string, or if trying to match a word.
### /addpaidsigncondition
@ -57,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
@ -76,3 +90,18 @@ Removes a registered paid sign
* paidsigns.manage - Grants the permission to add/remove a paid sign
* paidsigns.reload - Grants the permissions to reload the plugin
* paidsigns.paymentexempt - Makes this player exempt from the cost of paid signs
## Configuration options
* language - The language to use for all messages displayed to players
* ignoreCase - Whether to ignore the case (lowercase/uppercase) of the paid sign 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 are case-sensitive or not.
* 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.
* 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)
* 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.2.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,17 +4,24 @@ 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;
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.SignBreakListener;
import net.knarcraft.paidsigns.listener.SignListener;
import net.knarcraft.paidsigns.manager.EconomyManager;
import net.knarcraft.paidsigns.manager.PaidSignManager;
import net.knarcraft.paidsigns.manager.TrackedSignManager;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;
import org.bukkit.command.TabExecutor;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.plugin.PluginManager;
@ -29,8 +36,12 @@ public final class PaidSigns extends JavaPlugin {
private static PaidSigns paidSigns;
private PaidSignManager signManager;
private String language;
private boolean ignoreCase;
private boolean ignoreColor;
private boolean refundsEnabled;
private short refundPercentage;
private boolean refundAlways;
/**
* Instantiates a new paid signs object
@ -52,11 +63,14 @@ public final class PaidSigns extends JavaPlugin {
@Override
public void onEnable() {
setupVault();
signManager = new PaidSignManager(PaidSignManager.loadSigns());
loadConfig();
Translator.loadLanguages(language);
signManager = new PaidSignManager(PaidSignManager.loadSigns());
TrackedSignManager.loadTrackedSigns();
PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(new SignListener(), this);
pluginManager.registerEvents(new SignBreakListener(), this);
registerCommands();
}
@ -71,7 +85,9 @@ public final class PaidSigns extends JavaPlugin {
public void reload() {
this.reloadConfig();
loadConfig();
Translator.loadLanguages(language);
signManager = new PaidSignManager(PaidSignManager.loadSigns());
TrackedSignManager.loadTrackedSigns();
}
/**
@ -101,46 +117,67 @@ public final class PaidSigns extends JavaPlugin {
return this.ignoreColor;
}
/**
* Checks whether refunds are currently enabled
*
* @return <p>Whether refunds are currently enabled</p>
*/
public boolean areRefundsEnabled() {
return this.refundsEnabled;
}
/**
* Gets the percentage of the initial cost to refund the sign creator
*
* @return <p>The percentage of the cost to refund</p>
*/
public short getRefundPercentage() {
if (this.refundPercentage < 0) {
return 0;
} else if (refundPercentage > 100) {
return 100;
}
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
*/
private void registerCommands() {
PluginCommand addCommand = this.getCommand("addPaidSign");
if (addCommand != null) {
addCommand.setExecutor(new AddCommand());
addCommand.setTabCompleter(new AddTabCompleter());
}
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("editPaidSign", new EditCommand(), new EditTabCompleter());
PluginCommand listCommand = this.getCommand("listPaidSigns");
if (listCommand != null) {
listCommand.setExecutor(new ListCommand());
listCommand.setTabCompleter(new ListTabCompleter());
}
TabExecutor removeTabExecutor = new RemoveTabCommand();
registerCommand("removePaidSign", removeTabExecutor, removeTabExecutor);
TabExecutor reloadTabExecutor = new ReloadTabCommand();
registerCommand("reload", reloadTabExecutor, reloadTabExecutor);
}
PluginCommand addConditionCommand = this.getCommand("addPaidSignCondition");
if (addConditionCommand != null) {
addConditionCommand.setExecutor(new AddConditionCommand());
addConditionCommand.setTabCompleter(new AddConditionTabCompleter());
}
PluginCommand removeConditionCommand = this.getCommand("removePaidSignCondition");
if (removeConditionCommand != null) {
removeConditionCommand.setExecutor(new RemoveConditionCommand());
removeConditionCommand.setTabCompleter(new RemoveConditionTabCompleter());
}
PluginCommand removeCommand = this.getCommand("removePaidSign");
if (removeCommand != null) {
TabExecutor removeTabExecutor = new RemoveTabCommand();
removeCommand.setExecutor(removeTabExecutor);
removeCommand.setTabCompleter(removeTabExecutor);
}
PluginCommand reloadCommand = this.getCommand("reload");
if (reloadCommand != null) {
TabExecutor reloadTabExecutor = new ReloadTabCommand();
reloadCommand.setExecutor(reloadTabExecutor);
reloadCommand.setTabCompleter(reloadTabExecutor);
/**
* Registers a command if possible
*
* @param command <p>The command to register</p>
* @param commandExecutor <p>The command executor for executing the command</p>
* @param tabCompleter <p>The tab completer for tab-completing the command</p>
*/
private void registerCommand(String command, CommandExecutor commandExecutor, TabCompleter tabCompleter) {
PluginCommand pluginCommand = this.getCommand(command);
if (pluginCommand != null) {
pluginCommand.setExecutor(commandExecutor);
pluginCommand.setTabCompleter(tabCompleter);
}
}
@ -151,8 +188,13 @@ public final class PaidSigns extends JavaPlugin {
FileConfiguration config = this.getConfig();
config.options().copyDefaults(true);
this.saveDefaultConfig();
this.saveConfig();
language = config.getString("language", "en");
ignoreCase = config.getBoolean("ignoreCase", true);
ignoreColor = config.getBoolean("ignoreColor", false);
refundsEnabled = config.getBoolean("refundsEnabled", true);
refundPercentage = (short) config.getInt("refundPercentage", 100);
refundAlways = config.getBoolean("refundAlways", false);
}
/**
@ -164,7 +206,7 @@ public final class PaidSigns extends JavaPlugin {
if (economyProvider != null) {
EconomyManager.initialize(economyProvider.getProvider());
} else {
throw new IllegalStateException("[PaidSigns] Error: Vault could not be loaded");
throw new IllegalStateException("Error: Vault could not be loaded");
}
}

View File

@ -2,6 +2,8 @@ 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.manager.PaidSignManager;
import net.knarcraft.paidsigns.property.OptionState;
import org.bukkit.command.Command;
@ -9,9 +11,6 @@ import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A representation of the command for adding a new paid sign
@ -31,7 +30,7 @@ public class AddCommand extends TokenizedCommand {
try {
cost = Double.parseDouble(arguments.get(1));
} catch (NumberFormatException exception) {
sender.sendMessage("You provided an invalid number");
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_INVALID_NUMBER));
return false;
}
String permission = "";
@ -46,42 +45,44 @@ public class AddCommand extends TokenizedCommand {
if (argumentSize > 4) {
ignoreColor = OptionState.fromString(arguments.get(4));
}
boolean matchAnyCondition = false;
if (argumentSize > 5) {
matchAnyCondition = Boolean.parseBoolean(arguments.get(5));
}
return createPaidSign(sender, signName, cost, permission, ignoreCase, ignoreColor);
return createPaidSign(sender, signName, cost, permission, ignoreCase, ignoreColor, matchAnyCondition);
}
/**
* Creates a new paid sign with the given user input
*
* @param sender <p>The command sender that called the add command</p>
* @param signName <p>The name of the new paid sign</p>
* @param cost <p>The cost of the new paid sign</p>
* @param permission <p>The permission required for creating the sign represented by the paid sign</p>
* @param ignoreCase <p>Whether to ignore case for the paid sign's conditions</p>
* @param ignoreColor <p>Whether to ignore color for the paid sign's conditions</p>
* @param sender <p>The command sender that called the add command</p>
* @param signName <p>The name of the new paid sign</p>
* @param cost <p>The cost of the new paid sign</p>
* @param permission <p>The permission required for creating the sign represented by the paid sign</p>
* @param ignoreCase <p>Whether to ignore case for the paid sign's conditions</p>
* @param ignoreColor <p>Whether to ignore color for the paid sign's conditions</p>
* @param matchAnyCondition <p>Whether to treat any matching condition as a sign match</p>
* @return <p>True if the paid sign was successfully created and registered</p>
*/
private boolean createPaidSign(CommandSender sender, String signName, double cost, String permission,
OptionState ignoreCase, OptionState ignoreColor) {
OptionState ignoreCase, OptionState ignoreColor, boolean matchAnyCondition) {
PaidSignManager manager = PaidSigns.getInstance().getSignManager();
try {
PaidSign sign = new PaidSign(signName, cost, permission, ignoreCase, ignoreColor);
PaidSign sign = new PaidSign(signName, cost, permission, ignoreCase, ignoreColor, matchAnyCondition);
if (manager.getPaidSign(signName) != null) {
sender.sendMessage("A paid sign with the same name already exists");
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_NAME_DUPLICATE));
return false;
}
manager.addPaidSign(sign);
sender.sendMessage("Successfully added new paid sign");
sender.sendMessage(StringFormatter.getTranslatedInfoMessage(TranslatableMessage.SUCCESS_ADDED_PAID_SIGN));
return true;
} catch (IOException e) {
Logger logger = PaidSigns.getInstance().getLogger();
logger.log(Level.SEVERE, "Exception encountered while trying to write " +
"to the data file");
logger.log(Level.SEVERE, Arrays.toString(e.getStackTrace()));
sender.sendMessage("An exception occurred. Please notify the server administrator or check the server log.");
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_EXCEPTION_OCCURRED));
return false;
} catch (IllegalArgumentException e) {
sender.sendMessage("Invalid input: " + e.getMessage());
sender.sendMessage(StringFormatter.replacePlaceholder(StringFormatter.getTranslatedErrorMessage(
TranslatableMessage.ERROR_INVALID_INPUT), "{input}", e.getMessage()));
}
return false;
}

View File

@ -2,6 +2,8 @@ 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.manager.PaidSignManager;
import net.knarcraft.paidsigns.property.OptionState;
import org.bukkit.command.Command;
@ -9,9 +11,6 @@ import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A representation of the command for adding a new match condition for a sign
@ -19,7 +18,8 @@ import java.util.logging.Logger;
public class AddConditionCommand extends TokenizedCommand {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
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;
@ -29,14 +29,21 @@ public class AddConditionCommand extends TokenizedCommand {
short lineNumber;
try {
lineNumber = (short) (Short.parseShort(arguments.get(1)) - 1);
if (lineNumber < 0 || lineNumber > 3) {
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_INVALID_NUMBER));
return false;
}
} catch (NumberFormatException exception) {
sender.sendMessage("Invalid line number given");
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_INVALID_NUMBER));
return false;
}
String stringToMatch = arguments.get(2);
boolean executeRegEx = false;
if (argumentSize > 3) {
executeRegEx = Boolean.parseBoolean(arguments.get(3));
if (executeRegEx && isRegExInvalid(sender, stringToMatch)) {
return false;
}
}
OptionState ignoreCase = OptionState.DEFAULT;
if (argumentSize > 4) {
@ -46,23 +53,38 @@ public class AddConditionCommand extends TokenizedCommand {
if (argumentSize > 5) {
ignoreColor = OptionState.getFromBoolean(Boolean.parseBoolean(arguments.get(5)));
}
return addCondition(name, lineNumber, stringToMatch, executeRegEx, ignoreCase, ignoreColor, sender);
}
/**
* Uses the given input to add a paid sign condition
*
* @param name <p>The name of the paid sign to add the condition to</p>
* @param lineNumber <p>The sign line the condition should check</p>
* @param stringToMatch <p>The string to look for on created signs</p>
* @param executeRegEx <p>Whether to treat the match string as a regular expression</p>
* @param ignoreCase <p>Whether to ignore case when matching</p>
* @param ignoreColor <p>Whether to ignore color when matching</p>
* @param sender <p>The command sender to notify when finished</p>
* @return <p>True if the condition was successfully added</p>
*/
private boolean addCondition(String name, short lineNumber, String stringToMatch, boolean executeRegEx,
OptionState ignoreCase, OptionState ignoreColor, CommandSender sender) {
PaidSignManager signManager = PaidSigns.getInstance().getSignManager();
PaidSign sign = signManager.getPaidSign(name);
if (sign == null) {
sender.sendMessage("No such paid sign exists");
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_PAID_SIGN_NOT_FOUND));
return false;
}
sign.addCondition(lineNumber, stringToMatch, executeRegEx, ignoreCase, ignoreColor);
try {
signManager.saveSigns();
} catch (IOException e) {
Logger logger = PaidSigns.getInstance().getLogger();
logger.log(Level.SEVERE, "Exception encountered while trying to write to the data file");
logger.log(Level.SEVERE, Arrays.toString(e.getStackTrace()));
sender.sendMessage("An exception occurred. Please notify the server administrator or check the server log.");
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_EXCEPTION_OCCURRED));
return false;
}
sender.sendMessage("Condition added");
sender.sendMessage(StringFormatter.getTranslatedInfoMessage(
TranslatableMessage.SUCCESS_ADDED_PAID_SIGN_CONDITION));
return true;
}

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,9 +16,8 @@ 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;
@Nullable
@Override
@ -32,7 +26,6 @@ public class AddTabCompleter extends TokenizedTabCompleter {
super.onTabComplete(sender, command, alias, args);
if (names == null) {
initializeValues();
loadAvailablePermissions();
}
if (argumentSize == 1) {
@ -40,90 +33,17 @@ 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) {
return TabCompleteHelper.filterMatchingStartsWith(options, arguments.get(4));
} else if (argumentSize == 6) {
return TabCompleteHelper.filterMatchingStartsWith(booleans, arguments.get(5));
}
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
*/
@ -131,6 +51,7 @@ public class AddTabCompleter extends TokenizedTabCompleter {
names = TabCompleteHelper.getExamplePaidSignNames();
costs = TabCompleteHelper.getCosts();
options = TabCompleteHelper.getOptionStates();
booleans = TabCompleteHelper.getBooleans();
}
}

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

@ -3,12 +3,20 @@ 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.formatting.Translator;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import static net.knarcraft.paidsigns.formatting.StringFormatter.getTranslatedErrorMessage;
import static net.knarcraft.paidsigns.formatting.StringFormatter.replacePlaceholder;
import static net.knarcraft.paidsigns.formatting.StringFormatter.replacePlaceholders;
import static net.knarcraft.paidsigns.formatting.StringFormatter.translateBoolean;
/**
* A representation of the command for listing information about paid signs
*/
@ -19,10 +27,7 @@ public class ListCommand extends TokenizedCommand {
@NotNull String[] args) {
super.onCommand(sender, command, label, args);
if (argumentSize < 1) {
sender.sendMessage("Paid signs:");
for (String signName : PaidSigns.getInstance().getSignManager().getAllPaidSigns().keySet()) {
sender.sendMessage(" | " + signName);
}
displaySigns(sender);
return true;
} else if (argumentSize < 3) {
return parsePaidSignSelection(sender);
@ -30,6 +35,21 @@ public class ListCommand extends TokenizedCommand {
return false;
}
/**
* Displays all available signs
*
* @param sender <p>The command sender to display the signs to</p>
*/
private void displaySigns(CommandSender sender) {
StringBuilder signs = new StringBuilder();
for (String signName : PaidSigns.getInstance().getSignManager().getAllPaidSigns().keySet()) {
signs.append(StringFormatter.replacePlaceholder(Translator.getTranslatedMessage(
TranslatableMessage.PAID_SIGNS_INFO_FORMAT), "{name}", signName));
}
sender.sendMessage(StringFormatter.replacePlaceholder(Translator.getTranslatedMessage(
TranslatableMessage.PAID_SIGNS_INFO), "{signs}", signs.toString()));
}
/**
* Parses the given input and displays the wanted paid-sign information
*
@ -39,25 +59,30 @@ public class ListCommand extends TokenizedCommand {
private boolean parsePaidSignSelection(CommandSender sender) {
PaidSign paidSign = PaidSigns.getInstance().getSignManager().getPaidSign(arguments.get(0));
if (paidSign == null) {
sender.sendMessage("No such paid sign");
sender.sendMessage(getTranslatedErrorMessage(TranslatableMessage.ERROR_PAID_SIGN_NOT_FOUND));
return false;
}
if (argumentSize < 2) {
displayPaidSign(sender, paidSign);
} else {
short signLine;
short lineNumber;
try {
signLine = (short) (Short.parseShort(arguments.get(1)) - 1);
lineNumber = (short) (Short.parseShort(arguments.get(1)) - 1);
if (lineNumber < 0 || lineNumber > 3) {
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_INVALID_NUMBER));
return false;
}
} catch (NumberFormatException exception) {
sender.sendMessage("Invalid number given");
sender.sendMessage(getTranslatedErrorMessage(TranslatableMessage.ERROR_INVALID_NUMBER));
return false;
}
if (!paidSign.getConditions().containsKey(signLine)) {
sender.sendMessage("The paid sign you specified has no condition for line " + signLine);
if (!paidSign.getConditions().containsKey(lineNumber)) {
sender.sendMessage(replacePlaceholder(getTranslatedErrorMessage(
TranslatableMessage.ERROR_NO_SUCH_CONDITION), "{line}", String.valueOf(lineNumber)));
return false;
}
PaidSignCondition condition = paidSign.getConditions().get(signLine);
displayPaidSignCondition(sender, paidSign.getName(), signLine, condition);
PaidSignCondition condition = paidSign.getConditions().get(lineNumber);
displayPaidSignCondition(sender, paidSign.getName(), lineNumber, condition);
}
return true;
}
@ -72,13 +97,11 @@ public class ListCommand extends TokenizedCommand {
*/
private void displayPaidSignCondition(CommandSender sender, String signName, short signLine,
PaidSignCondition condition) {
sender.sendMessage("Paid sign condition info: ");
sender.sendMessage("Paid sign name: " + signName);
sender.sendMessage("Condition line: " + signLine);
sender.sendMessage("Condition match string: " + condition.getStringToMatch());
sender.sendMessage("Execute RegEx: " + condition.executeRegex());
sender.sendMessage("Ignore case: " + condition.ignoreCase());
sender.sendMessage("Ignore color: " + condition.ignoreColor());
sender.sendMessage(StringFormatter.replacePlaceholders(Translator.getTranslatedMessage(
TranslatableMessage.PAID_SIGN_CONDITION_INFO), new String[]{"{name}", "{line}", "{match}", "{regex}",
"{case}", "{color}"}, new String[]{signName, String.valueOf(signLine + 1), condition.getStringToMatch(),
translateBoolean(condition.executeRegex()), translateBoolean(condition.ignoreCase()),
translateBoolean(condition.ignoreColor())}));
}
/**
@ -88,17 +111,20 @@ public class ListCommand extends TokenizedCommand {
* @param paidSign <p>The paid sign to display information about</p>
*/
private void displayPaidSign(CommandSender sender, PaidSign paidSign) {
sender.sendMessage("Paid sign info:");
sender.sendMessage("Name: " + paidSign.getName());
sender.sendMessage("Cost: " + paidSign.getCost());
sender.sendMessage("Permission: " + paidSign.getPermission());
sender.sendMessage("Ignore case: " + paidSign.getIgnoreCase());
sender.sendMessage("Ignore color: " + paidSign.getIgnoreColor());
sender.sendMessage("Sign conditions: ");
Map<Short, PaidSignCondition> conditions = paidSign.getConditions();
for (short lineIndex : conditions.keySet()) {
sender.sendMessage(" | " + (lineIndex + 1) + ". " + conditions.get(lineIndex).getStringToMatch());
Map<Short, PaidSignCondition> signConditions = paidSign.getConditions();
StringBuilder conditions = new StringBuilder();
for (short lineIndex : signConditions.keySet()) {
String format = Translator.getTranslatedMessage(TranslatableMessage.PAID_SIGN_INFO_CONDITION_FORMAT);
conditions.append(StringFormatter.replacePlaceholders(format, new String[]{"{line}", "{condition}"},
new String[]{String.valueOf((lineIndex + 1)), signConditions.get(lineIndex).getStringToMatch()}));
}
sender.sendMessage(replacePlaceholders(Translator.getTranslatedMessage(
TranslatableMessage.PAID_SIGN_INFO), new String[]{"{name}", "{cost}", "{permission}", "{case}",
"{color}", "{any}", "{conditions}"}, new String[]{paidSign.getName(), String.valueOf(paidSign.getCost()),
paidSign.getPermission(), translateBoolean(paidSign.getIgnoreCase()),
translateBoolean(paidSign.getIgnoreColor()), translateBoolean(paidSign.matchAnyCondition()),
conditions.toString()}));
}
}

View File

@ -1,6 +1,8 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.formatting.StringFormatter;
import net.knarcraft.paidsigns.formatting.TranslatableMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
@ -18,7 +20,8 @@ public class ReloadTabCommand implements TabExecutor {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
PaidSigns.getInstance().reload();
return false;
sender.sendMessage(StringFormatter.getTranslatedInfoMessage(TranslatableMessage.SUCCESS_RELOADED));
return true;
}
@Override

View File

@ -2,15 +2,14 @@ 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.manager.PaidSignManager;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A representation of the command for removing a condition from a sign
@ -25,36 +24,38 @@ public class RemoveConditionCommand extends TokenizedCommand {
}
String name = arguments.get(0);
short line;
short lineNumber;
try {
line = (short) (Short.parseShort(arguments.get(1)) - 1);
lineNumber = (short) (Short.parseShort(arguments.get(1)) - 1);
if (lineNumber < 0 || lineNumber > 3) {
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_INVALID_NUMBER));
return false;
}
} catch (NumberFormatException exception) {
sender.sendMessage("Invalid line number given");
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_INVALID_NUMBER));
return false;
}
PaidSignManager signManager = PaidSigns.getInstance().getSignManager();
PaidSign sign = signManager.getPaidSign(name);
if (sign == null) {
sender.sendMessage("Invalid sign name given");
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_PAID_SIGN_NOT_FOUND));
return false;
}
if (!sign.getConditions().containsKey(line)) {
sender.sendMessage("No condition registered for line " + (line + 1));
if (!sign.getConditions().containsKey(lineNumber)) {
sender.sendMessage(StringFormatter.replacePlaceholder(StringFormatter.getTranslatedErrorMessage(
TranslatableMessage.ERROR_NO_SUCH_CONDITION), "{line}", String.valueOf((lineNumber + 1))));
return false;
}
sign.removeCondition(line);
sign.removeCondition(lineNumber);
try {
signManager.saveSigns();
} catch (IOException e) {
Logger logger = PaidSigns.getInstance().getLogger();
logger.log(Level.SEVERE, "Exception encountered while trying to write to the data file");
logger.log(Level.SEVERE, Arrays.toString(e.getStackTrace()));
sender.sendMessage("An exception occurred. Please notify the server administrator or check the server log.");
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_EXCEPTION_OCCURRED));
return false;
}
sender.sendMessage("Condition removed");
sender.sendMessage(StringFormatter.getTranslatedInfoMessage(TranslatableMessage.SUCCESS_REMOVED_CONDITION));
return true;
}

View File

@ -1,7 +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;
@ -11,11 +13,7 @@ import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A representation of the command for removing a paid sign
@ -33,17 +31,15 @@ public class RemoveTabCommand implements TabExecutor {
try {
if (PaidSigns.getInstance().getSignManager().removePaidSign(name)) {
sender.sendMessage("Successfully removed paid sign");
sender.sendMessage(StringFormatter.getTranslatedInfoMessage(
TranslatableMessage.SUCCESS_REMOVED_PAID_SIGN));
} else {
sender.sendMessage("No matching paid sign was found");
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(
TranslatableMessage.ERROR_PAID_SIGN_NOT_FOUND));
}
return true;
} catch (IOException e) {
Logger logger = PaidSigns.getInstance().getLogger();
logger.log(Level.SEVERE, "Exception encountered while trying to write " +
"to the data file");
logger.log(Level.SEVERE, Arrays.toString(e.getStackTrace()));
sender.sendMessage("An exception occurred. Please notify the server administrator or check the server log.");
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_EXCEPTION_OCCURRED));
}
return false;
}
@ -56,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

@ -16,18 +16,21 @@ public class PaidSign {
private final String permission;
private final OptionState ignoreCase;
private final OptionState ignoreColor;
private final boolean matchAnyCondition;
private final Map<Short, PaidSignCondition> conditions = new HashMap<>();
/**
* Instantiates a new paid sign
*
* @param name <p>A recognizable, unique, name used to identify this paid sign</p>
* @param cost <p>The cost of creating this paid sign</p>
* @param permission <p>The permission required to create the sign this paid sign matches</p>
* @param ignoreCase <p>Whether to ignore case when looking for this permission sign</p>
* @param ignoreColor <p>Whether to ignore color when looking for this permission sign</p>
* @param name <p>A recognizable, unique, name used to identify this paid sign</p>
* @param cost <p>The cost of creating this paid sign</p>
* @param permission <p>The permission required to create the sign this paid sign matches</p>
* @param ignoreCase <p>Whether to ignore case when looking for this permission sign</p>
* @param ignoreColor <p>Whether to ignore color when looking for this permission sign</p>
* @param matchAnyCondition <p>Whether to treat a match for any condition as a sign match</p>
*/
public PaidSign(String name, double cost, String permission, OptionState ignoreCase, OptionState ignoreColor) {
public PaidSign(String name, double cost, String permission, OptionState ignoreCase, OptionState ignoreColor,
boolean matchAnyCondition) {
if (name == null || name.trim().isBlank()) {
throw new IllegalArgumentException("Name cannot be empty");
}
@ -42,6 +45,7 @@ public class PaidSign {
this.permission = permission;
this.ignoreCase = ignoreCase;
this.ignoreColor = ignoreColor;
this.matchAnyCondition = matchAnyCondition;
}
/**
@ -98,6 +102,15 @@ public class PaidSign {
return OptionState.getBooleanValue(this.ignoreColor, PaidSigns.getInstance().ignoreColor());
}
/**
* Gets whether to treat a match for any condition as a sign match
*
* @return <p>Whether to treat a match for any condition as a sign match</p>
*/
public boolean matchAnyCondition() {
return this.matchAnyCondition;
}
/**
* Checks whether this paid sign matches the given set of sign lines
*
@ -109,11 +122,18 @@ public class PaidSign {
if (this.conditions.isEmpty()) {
return false;
}
boolean success = true;
boolean matchAny = matchAnyCondition();
boolean success = !matchAny;
for (short i = 0; i < 4; i++) {
PaidSignCondition condition = this.conditions.get(i);
if (condition != null) {
success = success && condition.test(lines[i]);
boolean conditionMatches = condition.test(lines[i]);
if (matchAny) {
success |= conditionMatches;
} else {
success &= conditionMatches;
}
}
}
return success;

View File

@ -0,0 +1,42 @@
package net.knarcraft.paidsigns.container;
import java.util.UUID;
/**
* A representation of a sign placed by a player that matched a paid sign
*/
public class TrackedSign {
private final UUID playerId;
private final double cost;
/**
* Instantiates a new tracked sign
*
* @param playerId <p>The unique id of the player that created the sign</p>
* @param cost <p>The cost the player paid for creating the sign</p>
*/
public TrackedSign(UUID playerId, double cost) {
this.playerId = playerId;
this.cost = cost;
}
/**
* Gets the id of the player that created this tracked sign
*
* @return <p>The player that created this tracked sign</p>
*/
public UUID getPlayerId() {
return this.playerId;
}
/**
* Gets the cost the player paid for creating this paid sign
*
* @return <p>The cost paid for creating this sign</p>
*/
public double getCost() {
return this.cost;
}
}

View File

@ -0,0 +1,125 @@
package net.knarcraft.paidsigns.formatting;
import net.md_5.bungee.api.ChatColor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A formatter for formatting displayed messages
*/
public final class StringFormatter {
private StringFormatter() {
}
/**
* Translates the given boolean value
*
* @param booleanValue <p>The boolean value to translate</p>
* @return <p>The translation of the boolean value</p>
*/
public static String translateBoolean(boolean booleanValue) {
if (booleanValue) {
return Translator.getTranslatedMessage(TranslatableMessage.BOOLEAN_TRUE);
} else {
return Translator.getTranslatedMessage(TranslatableMessage.BOOLEAN_FALSE);
}
}
/**
* Replaces a placeholder in a string
*
* @param input <p>The input string to replace in</p>
* @param placeholder <p>The placeholder to replace</p>
* @param replacement <p>The replacement value</p>
* @return <p>The input string with the placeholder replaced</p>
*/
public static String replacePlaceholder(String input, String placeholder, String replacement) {
return input.replace(placeholder, replacement);
}
/**
* Replaces placeholders in a string
*
* @param input <p>The input string to replace in</p>
* @param placeholders <p>The placeholders to replace</p>
* @param replacements <p>The replacement values</p>
* @return <p>The input string with placeholders replaced</p>
*/
public static String replacePlaceholders(String input, String[] placeholders, String[] replacements) {
for (int i = 0; i < Math.min(placeholders.length, replacements.length); i++) {
input = replacePlaceholder(input, placeholders[i], replacements[i]);
}
return input;
}
/**
* Gets a translated and formatted info message
*
* @param translatableMessage <p>The translatable message to translate and format</p>
* @return <p>The translated and formatted message</p>
*/
public static String getTranslatedInfoMessage(TranslatableMessage translatableMessage) {
return formatInfoMessage(Translator.getTranslatedMessage(translatableMessage));
}
/**
* Gets a translated and formatted error message
*
* @param translatableMessage <p>The translatable message to translate and format</p>
* @return <p>The translated and formatted message</p>
*/
public static String getTranslatedErrorMessage(TranslatableMessage translatableMessage) {
return formatErrorMessage(Translator.getTranslatedMessage(translatableMessage));
}
/**
* Formats an information message by adding the prefix and text color
*
* @param message <p>The message to format</p>
* @return <p>The formatted message</p>
*/
public static String formatInfoMessage(String message) {
return ChatColor.DARK_GREEN + formatMessage(message);
}
/**
* Formats an error message by adding the prefix and text color
*
* @param message <p>The message to format</p>
* @return <p>The formatted message</p>
*/
public static String formatErrorMessage(String message) {
return ChatColor.DARK_RED + formatMessage(message);
}
/**
* Translates all found color codes to formatting in a string
*
* @param message <p>The string to search for color codes</p>
* @return <p>The message with color codes translated</p>
*/
public static String translateAllColorCodes(String message) {
message = ChatColor.translateAlternateColorCodes('&', message);
Pattern pattern = Pattern.compile("(#[a-fA-F0-9]{6})");
Matcher matcher = pattern.matcher(message);
while (matcher.find()) {
message = message.replace(matcher.group(), "" + ChatColor.of(matcher.group()));
}
return message;
}
/**
* Formats a message by adding the prefix and text color
*
* @param message <p>The message to format</p>
* @return <p>The formatted message</p>
*/
private static String formatMessage(String message) {
return Translator.getTranslatedMessage(TranslatableMessage.PREFIX) + " " +
ChatColor.RESET + message;
}
}

View File

@ -0,0 +1,138 @@
package net.knarcraft.paidsigns.formatting;
/**
* An enum representing all translatable messages
*/
public enum TranslatableMessage {
/**
* The prefix to display in messages
*/
PREFIX,
/**
* The message to display when a paid sign is successfully added
*/
SUCCESS_ADDED_PAID_SIGN,
/**
* The message to display when a paid sign condition is successfully added
*/
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
*/
SUCCESS_REMOVED_PAID_SIGN,
/**
* The message to display when a paid sign condition has been successfully removed
*/
SUCCESS_REMOVED_CONDITION,
/**
* The message to display after the plugin has been reloaded
*/
SUCCESS_RELOADED,
/**
* The message to display when a player has been charged for creating a paid sign
*/
SUCCESS_PAID_FOR_SIGN,
/**
* The message to display when a player has been refunded for breaking a sign
*/
SUCCESS_REFUNDED,
/**
* The info text used to display all paid signs
*/
PAID_SIGNS_INFO,
/**
* The format used for displaying a paid sign's name
*/
PAID_SIGNS_INFO_FORMAT,
/**
* The info text used to display information about a paid sign
*/
PAID_SIGN_INFO,
/**
* The info text used to display information about a paid sign condition
*/
PAID_SIGN_CONDITION_INFO,
/**
* The format used for displaying one of a paid sign's conditions
*/
PAID_SIGN_INFO_CONDITION_FORMAT,
/**
* The message to display for a true boolean value
*/
BOOLEAN_TRUE,
/**
* The message to display for a false boolean value
*/
BOOLEAN_FALSE,
/**
* The error to display when a command argument contains an invalid number
*/
ERROR_INVALID_NUMBER,
/**
* The error to display if a paid sign name duplicate is encountered
*/
ERROR_NAME_DUPLICATE,
/**
* The error to display if a severe exception occurs
*/
ERROR_EXCEPTION_OCCURRED,
/**
* The error to display if some input is invalid
*/
ERROR_INVALID_INPUT,
/**
* The error to display if a specified paid sign is not found
*/
ERROR_PAID_SIGN_NOT_FOUND,
/**
* The error to display if a paid sign condition is specified, but does not exist
*/
ERROR_NO_SUCH_CONDITION,
/**
* The error to display if a player cannot pay for a sign matching a paid sign
*/
ERROR_CANNOT_AFFORD,
/**
* The error to display if an invalid regular expression is provided
*/
ERROR_INVALID_REGULAR_EXPRESSION,
/**
* The error to display if an invalid property is given to the edit command
*/
ERROR_PROPERTY_NOT_RECOGNIZED,
}

View File

@ -0,0 +1,118 @@
package net.knarcraft.paidsigns.formatting;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.utility.FileHelper;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
/**
* A tool to get strings translated to the correct language
*/
public final class Translator {
private static Map<TranslatableMessage, String> translatedMessages;
private static Map<TranslatableMessage, String> backupTranslatedMessages;
private Translator() {
}
/**
* Loads the languages used by this translator
*/
public static void loadLanguages(String selectedLanguage) {
backupTranslatedMessages = loadTranslatedMessages("en");
translatedMessages = loadCustomTranslatedMessages(selectedLanguage);
if (translatedMessages == null) {
translatedMessages = loadTranslatedMessages(selectedLanguage);
}
}
/**
* Gets a translated version of the given translatable message
*
* @param translatableMessage <p>The message to translate</p>
* @return <p>The translated message</p>
*/
public static String getTranslatedMessage(TranslatableMessage translatableMessage) {
if (translatedMessages == null) {
return "Translated strings not loaded";
}
String translatedMessage;
if (translatedMessages.containsKey(translatableMessage)) {
translatedMessage = translatedMessages.get(translatableMessage);
} else if (backupTranslatedMessages.containsKey(translatableMessage)) {
translatedMessage = backupTranslatedMessages.get(translatableMessage);
} else {
translatedMessage = translatableMessage.toString();
}
return StringFormatter.translateAllColorCodes(translatedMessage);
}
/**
* Loads all translated messages for the given language
*
* @param language <p>The language chosen by the user</p>
* @return <p>A mapping of all strings for the given language</p>
*/
public static Map<TranslatableMessage, String> loadTranslatedMessages(String language) {
try {
BufferedReader reader = FileHelper.getBufferedReaderForInternalFile("/strings.yml");
return loadTranslatableMessages(language, reader);
} catch (FileNotFoundException e) {
PaidSigns.getInstance().getLogger().log(Level.SEVERE, "Unable to load translated messages");
return null;
}
}
/**
* Tries to load translated messages from a custom strings.yml file
*
* @param language <p>The selected language</p>
* @return <p>The loaded translated strings, or null if no custom language file exists</p>
*/
public static Map<TranslatableMessage, String> loadCustomTranslatedMessages(String language) {
File strings = new File(PaidSigns.getInstance().getDataFolder(), "strings.yml");
if (!strings.exists()) {
PaidSigns.getInstance().getLogger().log(Level.FINEST, "Strings file not found");
return null;
}
try {
PaidSigns.getInstance().getLogger().log(Level.WARNING, "Loading custom strings...");
return loadTranslatableMessages(language, new BufferedReader(new InputStreamReader(new FileInputStream(strings))));
} catch (FileNotFoundException e) {
PaidSigns.getInstance().getLogger().log(Level.WARNING, "Unable to load custom messages");
return null;
}
}
/**
* Loads translatable messages from the given reader
*
* @param language <p>The selected language</p>
* @param reader <p>The buffered reader to read from</p>
* @return <p>The loaded translated strings</p>
*/
private static Map<TranslatableMessage, String> loadTranslatableMessages(String language, BufferedReader reader) {
Map<TranslatableMessage, String> translatedMessages = new HashMap<>();
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(reader);
for (TranslatableMessage message : TranslatableMessage.values()) {
String translated = configuration.getString(language + "." + message.toString());
if (translated != null) {
translatedMessages.put(message, translated);
}
}
return translatedMessages;
}
}

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

@ -2,13 +2,17 @@ package net.knarcraft.paidsigns.listener;
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.manager.EconomyManager;
import net.knarcraft.paidsigns.manager.TrackedSignManager;
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.SignChangeEvent;
import java.io.IOException;
import java.util.Map;
/**
@ -50,19 +54,36 @@ public class SignListener implements Listener {
return true;
}
double cost = paidSign.getCost();
boolean canAfford = EconomyManager.canAfford(player, cost);
if (!canAfford) {
player.sendMessage("[PaidSigns] You cannot afford to create this sign");
event.setCancelled(true);
} else {
String unit = EconomyManager.getCurrency(cost != 1);
player.sendMessage(String.format("[PaidSigns] You paid %.2f %s to create the sign", cost, unit));
EconomyManager.withdraw(player, cost);
}
performPaidSignTransaction(paidSign, player, event);
return true;
}
return false;
}
/**
* Performs the transaction to pay for the paid sign
*
* @param paidSign <p>The paid sign a match has been found for</p>
* @param player <p>The player that created the sign</p>
* @param event <p>The sign change event that caused the sign to be created</p>
*/
private void performPaidSignTransaction(PaidSign paidSign, Player player, SignChangeEvent event) {
double cost = paidSign.getCost();
boolean canAfford = EconomyManager.canAfford(player, cost);
if (!canAfford) {
player.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_CANNOT_AFFORD));
event.setCancelled(true);
} else {
String unit = EconomyManager.getCurrency(cost != 1);
EconomyManager.withdraw(player, cost);
player.sendMessage(String.format(StringFormatter.replacePlaceholders(
StringFormatter.getTranslatedInfoMessage(TranslatableMessage.SUCCESS_PAID_FOR_SIGN),
new String[]{"{cost}", "{unit}"}, new String[]{"%.2f", "%s"}), cost, unit));
try {
TrackedSignManager.addTrackedSign(event.getBlock().getLocation(), player.getUniqueId(), cost);
} catch (IOException ignored) {
}
}
}
}

View File

@ -2,7 +2,6 @@ package net.knarcraft.paidsigns.manager;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
/**
* A manager that performs all Economy tasks
@ -55,8 +54,18 @@ public final class EconomyManager {
* @param player <p>The player to withdraw money from</p>
* @param cost <p>The amount of money to withdraw</p>
*/
public static void withdraw(Player player, double cost) {
public static void withdraw(OfflinePlayer player, double cost) {
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

@ -9,9 +9,11 @@ import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A manager that keeps track of all registered paid signs
@ -96,7 +98,8 @@ public final class PaidSignManager {
String permission = signSection.getString(name + ".permission");
OptionState ignoreCase = OptionState.getFromBoolean(signSection.getBoolean(name + ".ignoreCase"));
OptionState ignoreColor = OptionState.getFromBoolean(signSection.getBoolean(name + ".ignoreColor"));
PaidSign sign = new PaidSign(name, cost, permission, ignoreCase, ignoreColor);
boolean matchAnyCondition = signSection.getBoolean(name + ".matchAnyCondition");
PaidSign sign = new PaidSign(name, cost, permission, ignoreCase, ignoreColor, matchAnyCondition);
loadConditions(signSection, sign);
paidSigns.put(name, sign);
}
@ -119,26 +122,45 @@ public final class PaidSignManager {
* @throws IOException <p>If unable to write to the signs file</p>
*/
public static void saveSigns(Map<String, PaidSign> signs) throws IOException {
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(signsFile);
ConfigurationSection signSection = configuration.createSection("paidSigns");
try {
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(signsFile);
ConfigurationSection signSection = configuration.createSection("paidSigns");
for (PaidSign sign : signs.values()) {
String name = sign.getName();
signSection.set(name + ".cost", sign.getCost());
signSection.set(name + ".permission", sign.getPermission());
signSection.set(name + ".ignoreCase", sign.getIgnoreCase());
signSection.set(name + ".ignoreColor", sign.getIgnoreColor());
ConfigurationSection conditionsSection = signSection.createSection(name + ".conditions");
Map<Short, PaidSignCondition> signConditions = sign.getConditions();
for (short lineIndex : signConditions.keySet()) {
PaidSignCondition condition = signConditions.get(lineIndex);
conditionsSection.set(lineIndex + ".stringToMatch", condition.getStringToMatch());
conditionsSection.set(lineIndex + ".executeRegEx", condition.executeRegex());
conditionsSection.set(lineIndex + ".ignoreCase", condition.ignoreCase());
conditionsSection.set(lineIndex + ".ignoreColor", condition.ignoreColor());
for (PaidSign sign : signs.values()) {
saveSign(signSection, sign);
}
configuration.save(signsFile);
} catch (IOException exception) {
Logger logger = PaidSigns.getInstance().getLogger();
logger.log(Level.SEVERE, "Exception encountered while trying to write " +
"to the data file");
logger.log(Level.SEVERE, Arrays.toString(exception.getStackTrace()));
throw exception;
}
}
/**
* Saves the given sign to the given configuration section
*
* @param signSection <p>The configuration section to save the sign to</p>
* @param sign <p>The sign to save</p>
*/
private static void saveSign(ConfigurationSection signSection, PaidSign sign) {
String name = sign.getName();
signSection.set(name + ".cost", sign.getCost());
signSection.set(name + ".permission", sign.getPermission());
signSection.set(name + ".ignoreCase", sign.getIgnoreCase());
signSection.set(name + ".ignoreColor", sign.getIgnoreColor());
signSection.set(name + ".matchAnyCondition", sign.matchAnyCondition());
ConfigurationSection conditionsSection = signSection.createSection(name + ".conditions");
Map<Short, PaidSignCondition> signConditions = sign.getConditions();
for (short lineIndex : signConditions.keySet()) {
PaidSignCondition condition = signConditions.get(lineIndex);
conditionsSection.set(lineIndex + ".stringToMatch", condition.getStringToMatch());
conditionsSection.set(lineIndex + ".executeRegEx", condition.executeRegex());
conditionsSection.set(lineIndex + ".ignoreCase", condition.ignoreCase());
conditionsSection.set(lineIndex + ".ignoreColor", condition.ignoreColor());
}
configuration.save(signsFile);
}
/**

View File

@ -0,0 +1,150 @@
package net.knarcraft.paidsigns.manager;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.container.TrackedSign;
import net.knarcraft.paidsigns.formatting.StringFormatter;
import net.knarcraft.paidsigns.formatting.TranslatableMessage;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
import org.bukkit.block.Sign;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.logging.Level;
/**
* A manager for keeping track of plugin-signs created by players
*/
public final class TrackedSignManager {
private static Map<Location, TrackedSign> trackedSigns = new HashMap<>();
private static final File signsFile = new File(PaidSigns.getInstance().getDataFolder(), "data.yml");
private TrackedSignManager() {
}
/**
* Adds a tracked sign to the manager
*
* @param signLocation <p>The location the sign was created at</p>
* @param playerId <p>The unique id of the player that created the sign</p>
* @throws IOException <p>If unable to save the tracked signs</p>
*/
public static void addTrackedSign(Location signLocation, UUID playerId, double cost) throws IOException {
trackedSigns.put(signLocation, new TrackedSign(playerId, cost));
saveTrackedSigns();
}
/**
* 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, boolean refund) throws IOException {
if (!trackedSigns.containsKey(signLocation)) {
return;
}
TrackedSign trackedSign = trackedSigns.get(signLocation);
trackedSigns.remove(signLocation);
saveTrackedSigns();
if (!PaidSigns.getInstance().areRefundsEnabled() || !refund) {
return;
}
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(trackedSign.getPlayerId());
double refundSum = trackedSign.getCost() / 100 * PaidSigns.getInstance().getRefundPercentage();
EconomyManager.deposit(offlinePlayer, refundSum);
if (offlinePlayer instanceof Player player) {
player.sendMessage(String.format(StringFormatter.replacePlaceholders(
StringFormatter.getTranslatedInfoMessage(TranslatableMessage.SUCCESS_REFUNDED),
new String[]{"{cost}", "{unit}"}, new String[]{"%.2f", "%s"}), refundSum,
EconomyManager.getCurrency(refundSum != 1)));
}
}
/**
* Loads all tracked signs from the data file
*/
public static void loadTrackedSigns() {
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(signsFile);
ConfigurationSection signSection = configuration.getConfigurationSection("trackedSigns");
trackedSigns = new HashMap<>();
if (signSection == null) {
PaidSigns.getInstance().getLogger().log(Level.WARNING, "Tracked signs section not found in data.yml");
return;
}
for (String key : signSection.getKeys(false)) {
try {
loadSign(signSection, key);
} catch (InvalidConfigurationException e) {
PaidSigns.getInstance().getLogger().log(Level.SEVERE, "Unable to load sign " + key + ": " +
e.getMessage());
}
}
}
/**
* Loads a sign from the save file
*
* @param signSection <p>The configuration section containing signs</p>
* @param key <p>The sign key which is also the sign's location</p>
* @throws InvalidConfigurationException <p>If unable to load the sign</p>
*/
private static void loadSign(ConfigurationSection signSection, String key) throws InvalidConfigurationException {
String[] locationParts = key.split(",");
Location signLocation;
try {
signLocation = new Location(Bukkit.getWorld(UUID.fromString(locationParts[0])),
Double.parseDouble(locationParts[1]), Double.parseDouble(locationParts[2]),
Double.parseDouble(locationParts[3]));
} catch (NumberFormatException exception) {
throw new InvalidConfigurationException("Invalid sign coordinates");
}
//Prevent destroyed signs from being tracked indefinitely
if (!(signLocation.getBlock().getState() instanceof Sign)) {
PaidSigns.getInstance().getLogger().log(Level.WARNING, "The sign at " + signLocation + " no longer " +
"exists. Removing from sign tracker.");
return;
}
double cost = signSection.getDouble(key + ".cost");
UUID playerId = UUID.fromString(Objects.requireNonNull(signSection.getString(key + ".playerId")));
TrackedSign trackedSign = new TrackedSign(playerId, cost);
trackedSigns.put(signLocation, trackedSign);
}
/**
* Saves the managed tracked signs to the data file
*
* @throws IOException <p>If unable to write to the data file</p>
*/
private static void saveTrackedSigns() throws IOException {
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(signsFile);
ConfigurationSection signSection = configuration.createSection("trackedSigns");
for (Location signLocation : trackedSigns.keySet()) {
TrackedSign sign = trackedSigns.get(signLocation);
String locationString = Objects.requireNonNull(signLocation.getWorld()).getUID() + "," +
signLocation.getBlockX() + "," + signLocation.getBlockY() + "," + signLocation.getBlockZ();
signSection.set(locationString + ".cost", sign.getCost());
signSection.set(locationString + ".playerId", sign.getPlayerId().toString());
}
configuration.save(signsFile);
}
}

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

@ -0,0 +1,32 @@
package net.knarcraft.paidsigns.utility;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/**
* A helper class for dealing with files
*/
public final class FileHelper {
private FileHelper() {
}
/**
* Gets a buffered reader for
*
* @return <p>A buffered read for reading the file</p>
* @throws FileNotFoundException <p>If unable to get an input stream for the given file</p>
*/
public static BufferedReader getBufferedReaderForInternalFile(String file) throws FileNotFoundException {
InputStream inputStream = FileHelper.class.getResourceAsStream(file);
if (inputStream == null) {
throw new FileNotFoundException("Unable to read the given file");
}
return new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
}
}

View File

@ -1,37 +1,28 @@
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() {
}
/**
* Finds tab complete values that contain the typed text
*
* @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 that contain the player's typed text</p>
*/
public static List<String> filterMatchingContains(List<String> values, String typedText) {
List<String> configValues = new ArrayList<>();
for (String value : values) {
if (value.toLowerCase().contains(typedText.toLowerCase())) {
configValues.add(value);
}
}
return configValues;
}
/**
* Finds tab complete values that match the start of the typed text
*
@ -139,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

@ -1,8 +1,23 @@
# The currently enabled language. More languages can be added to the language file
language: en
# Whether to ignore the case (lowercase/uppercase) of the paid sign 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 are case-sensitive or not.
# this value is used if not specified. The correct value depends on whether the plugin signs it should match are
# case-sensitive or not.
ignoreCase: true
# 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.
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)
refundsEnabled: true
# The percentage of the paid sign cost to refund (0-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]
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

@ -0,0 +1,27 @@
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"
SUCCESS_PAID_FOR_SIGN: "&bYou paid &3{cost} {unit} &bto create the sign"
SUCCESS_REFUNDED: "&bYou were refunded &3{cost} {unit} &bfor your broken sign"
PAID_SIGNS_INFO: "&f---&3Paid signs&f---{signs}\n&f-----------"
PAID_SIGNS_INFO_FORMAT: "\n&f| &3{name}"
PAID_SIGN_INFO: "&f---&3Paid sign&f---\n&f| &bName: &3{name}\n&f| &bCost: &3{cost}\n&f| &bPermission: &3{permission}\n&f| &bIgnore case: &3{case}\n&f| &bIgnore color: &3{color}\n&f| &bMatch any condition: &3{any}\n&f| &bSign conditions: &3{conditions}\n&f---------------"
PAID_SIGN_INFO_CONDITION_FORMAT: "\n&f| &b{line}: &3{condition}"
PAID_SIGN_CONDITION_INFO: "&f---&3Paid sign condition&f---\n&f| &bPaid sign name: &3{name}\n&f| &bCondition line: &3{line}\n&f| &bMatch string: &3{match}\n&f| &bExecute RegEx: &3{regex}\n&f| &bIgnore case: &3{case}\n&f| &bIgnore color: &3{color}\n&f---------------"
BOOLEAN_TRUE: "&2true"
BOOLEAN_FALSE: "&4false"
ERROR_INVALID_NUMBER: "&bYou provided an invalid number"
ERROR_NAME_DUPLICATE: "&bA paid sign with the same name already exists"
ERROR_EXCEPTION_OCCURRED: "&bAn exception occurred. Please notify the server administrator or check the server log."
ERROR_INVALID_INPUT: "&bInvalid input: {input}"
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_PROPERTY_NOT_RECOGNIZED: "&bThe property you tried to change was not recognized"