diff --git a/src/main/java/net/knarcraft/paidsigns/command/EditCommand.java b/src/main/java/net/knarcraft/paidsigns/command/EditCommand.java index c5a8d67..ca7005e 100644 --- a/src/main/java/net/knarcraft/paidsigns/command/EditCommand.java +++ b/src/main/java/net/knarcraft/paidsigns/command/EditCommand.java @@ -130,7 +130,7 @@ public class EditCommand extends TokenizedCommand { 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(), + updatedSign.addCondition(line, condition.stringToMatch(), condition.executeRegex(), OptionState.getFromBoolean(condition.ignoreCase()), OptionState.getFromBoolean(condition.ignoreColor())); } @@ -154,7 +154,7 @@ public class EditCommand extends TokenizedCommand { PaidSignConditionProperty property, String newValue) { PaidSignCondition condition = sign.getConditions().get(conditionIndex); String stringToMatch = property == PaidSignConditionProperty.STRING_TO_MATCH ? newValue : - condition.getStringToMatch(); + condition.stringToMatch(); boolean executeRegEx = property == PaidSignConditionProperty.EXECUTE_REG_EX ? Boolean.parseBoolean(newValue) : condition.executeRegex(); boolean ignoreCase = property == PaidSignConditionProperty.IGNORE_CASE ? OptionState.getBooleanValue( diff --git a/src/main/java/net/knarcraft/paidsigns/command/ListCommand.java b/src/main/java/net/knarcraft/paidsigns/command/ListCommand.java index 15d3361..56717f6 100644 --- a/src/main/java/net/knarcraft/paidsigns/command/ListCommand.java +++ b/src/main/java/net/knarcraft/paidsigns/command/ListCommand.java @@ -107,7 +107,7 @@ public class ListCommand extends TokenizedCommand { PaidSignCondition condition) { 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(), + "{case}", "{color}"}, new String[]{signName, String.valueOf(signLine + 1), condition.stringToMatch(), translateBoolean(condition.executeRegex()), translateBoolean(condition.ignoreCase()), translateBoolean(condition.ignoreColor())})); } @@ -124,7 +124,7 @@ public class ListCommand extends TokenizedCommand { 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()})); + new String[]{String.valueOf((lineIndex + 1)), signConditions.get(lineIndex).stringToMatch()})); } sender.sendMessage(replacePlaceholders(Translator.getTranslatedMessage( diff --git a/src/main/java/net/knarcraft/paidsigns/container/PaidSign.java b/src/main/java/net/knarcraft/paidsigns/container/PaidSign.java index 18a3223..e16e4d8 100644 --- a/src/main/java/net/knarcraft/paidsigns/container/PaidSign.java +++ b/src/main/java/net/knarcraft/paidsigns/container/PaidSign.java @@ -148,13 +148,15 @@ public class PaidSign { * @param ignoreCase

Whether to ignore case when matching against the condition

* @param ignoreColor

Whether to ignore color when matching against the condition

*/ - public void addCondition(short line, String stringToMatch, boolean executeRegex, OptionState ignoreCase, OptionState ignoreColor) { + public void addCondition(short line, String stringToMatch, boolean executeRegex, OptionState ignoreCase, + OptionState ignoreColor) { if (line < 0 || line > 3) { throw new IllegalArgumentException("Invalid sign line given for new paid sign condition"); } boolean ignoreCaseBoolean = OptionState.getBooleanValue(ignoreCase, this.getIgnoreCase()); boolean ignoreColorBoolean = OptionState.getBooleanValue(ignoreColor, this.getIgnoreColor()); - this.conditions.put(line, new PaidSignCondition(stringToMatch, executeRegex, ignoreCaseBoolean, ignoreColorBoolean)); + this.conditions.put(line, new PaidSignCondition(stringToMatch, executeRegex, ignoreCaseBoolean, + ignoreColorBoolean)); } /** diff --git a/src/main/java/net/knarcraft/paidsigns/container/PaidSignCondition.java b/src/main/java/net/knarcraft/paidsigns/container/PaidSignCondition.java index 7cf7df5..9ac4b7b 100644 --- a/src/main/java/net/knarcraft/paidsigns/container/PaidSignCondition.java +++ b/src/main/java/net/knarcraft/paidsigns/container/PaidSignCondition.java @@ -4,64 +4,14 @@ import net.knarcraft.paidsigns.utility.ColorHelper; /** * A condition for deciding if a paid sign matches a sign line + * + * @param stringToMatch

The string/regular expression the line has to match to fulfill this condition

+ * @param executeRegex

Whether to execute the match string as a regular expression

+ * @param ignoreCase

Whether to ignore uppercase/lowercase when comparing against this condition

+ * @param ignoreColor

Whether to ignore color codes when comparing against this condition

*/ -public class PaidSignCondition { - - final String stringToMatch; - final boolean executeRegex; - final boolean ignoreCase; - final boolean ignoreColor; - - /** - * Instantiates a new paid sign condition - * - * @param stringToMatch

The string/regular expression the line has to match to fulfill this condition

- * @param executeRegex

Whether to execute the match string as a regular expression

- * @param ignoreCase

Whether to ignore uppercase/lowercase when comparing against this condition

- * @param ignoreColor

Whether to ignore color codes when comparing against this condition

- */ - public PaidSignCondition(String stringToMatch, boolean executeRegex, boolean ignoreCase, boolean ignoreColor) { - this.stringToMatch = stringToMatch; - this.executeRegex = executeRegex; - this.ignoreCase = ignoreCase; - this.ignoreColor = ignoreColor; - } - - /** - * Gets the string this condition should match - * - * @return

The string this condition should match

- */ - public String getStringToMatch() { - return this.stringToMatch; - } - - /** - * Gets whether to execute the match string as RegEx - * - * @return

Whether to execute the match string as RegEx

- */ - public boolean executeRegex() { - return this.executeRegex; - } - - /** - * Gets whether to ignore case when trying to match strings - * - * @return

Whether to ignore case when trying to match strings

- */ - public boolean ignoreCase() { - return this.ignoreCase; - } - - /** - * Gets whether to ignore color when trying to match strings - * - * @return

Whether to ignore color when trying to match strings

- */ - public boolean ignoreColor() { - return this.ignoreColor; - } +public record PaidSignCondition(String stringToMatch, boolean executeRegex, boolean ignoreCase, + boolean ignoreColor) { /** * Tests whether the given line matches this condition diff --git a/src/main/java/net/knarcraft/paidsigns/container/PaidSignConditionMatch.java b/src/main/java/net/knarcraft/paidsigns/container/PaidSignConditionMatch.java new file mode 100644 index 0000000..8a28c0d --- /dev/null +++ b/src/main/java/net/knarcraft/paidsigns/container/PaidSignConditionMatch.java @@ -0,0 +1,19 @@ +package net.knarcraft.paidsigns.container; + +import org.jetbrains.annotations.NotNull; + +/** + * A container class for number of condition matches for a paid sign + * + * @param paidSign

The paid sign this class keeps track of matches for

+ * @param conditionMatches

The number of conditions matched for the paid sign

+ */ +public record PaidSignConditionMatch(PaidSign paidSign, + int conditionMatches) implements Comparable { + + @Override + public int compareTo(@NotNull PaidSignConditionMatch other) { + return Integer.compare(this.conditionMatches, other.conditionMatches); + } + +} diff --git a/src/main/java/net/knarcraft/paidsigns/container/TrackedSign.java b/src/main/java/net/knarcraft/paidsigns/container/TrackedSign.java index a050ea7..0b5b290 100644 --- a/src/main/java/net/knarcraft/paidsigns/container/TrackedSign.java +++ b/src/main/java/net/knarcraft/paidsigns/container/TrackedSign.java @@ -4,39 +4,10 @@ import java.util.UUID; /** * A representation of a sign placed by a player that matched a paid sign + * + * @param playerId

The unique id of the player that created the sign

+ * @param cost

The cost the player paid for creating the sign

*/ -public class TrackedSign { - - private final UUID playerId; - private final double cost; - - /** - * Instantiates a new tracked sign - * - * @param playerId

The unique id of the player that created the sign

- * @param cost

The cost the player paid for creating the sign

- */ - public TrackedSign(UUID playerId, double cost) { - this.playerId = playerId; - this.cost = cost; - } - - /** - * Gets the id of the player that created this tracked sign - * - * @return

The player that created this tracked sign

- */ - public UUID getPlayerId() { - return this.playerId; - } - - /** - * Gets the cost the player paid for creating this paid sign - * - * @return

The cost paid for creating this sign

- */ - public double getCost() { - return this.cost; - } +public record TrackedSign(UUID playerId, double cost) { } diff --git a/src/main/java/net/knarcraft/paidsigns/listener/SignListener.java b/src/main/java/net/knarcraft/paidsigns/listener/SignListener.java index d469966..a3db905 100644 --- a/src/main/java/net/knarcraft/paidsigns/listener/SignListener.java +++ b/src/main/java/net/knarcraft/paidsigns/listener/SignListener.java @@ -2,6 +2,7 @@ package net.knarcraft.paidsigns.listener; import net.knarcraft.paidsigns.PaidSigns; import net.knarcraft.paidsigns.container.PaidSign; +import net.knarcraft.paidsigns.container.PaidSignConditionMatch; import net.knarcraft.paidsigns.formatting.StringFormatter; import net.knarcraft.paidsigns.formatting.TranslatableMessage; import net.knarcraft.paidsigns.manager.EconomyManager; @@ -13,6 +14,9 @@ import org.bukkit.event.Listener; import org.bukkit.event.block.SignChangeEvent; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -28,36 +32,90 @@ public class SignListener implements Listener { } String[] lines = event.getLines(); - Map matchingSigns = PaidSigns.getInstance().getSignManager().getAllPaidSigns(); - for (PaidSign paidSign : matchingSigns.values()) { - //If a match is found, just return - if (matchSign(paidSign, lines, event)) { - return; - } + Map allPaidSigns = PaidSigns.getInstance().getSignManager().getAllPaidSigns(); + + //Generate the sorted list of the number of paid sign conditions matched for every paid sign + List allConditionMatches = new ArrayList<>(); + for (PaidSign paidSign : allPaidSigns.values()) { + calculateConditionMatches(paidSign, lines, allConditionMatches); + } + allConditionMatches.sort(Collections.reverseOrder()); + + //Make the player pay for the paid sign + PaidSign mostExpensive = getMatchingSign(allConditionMatches); + if (mostExpensive != null) { + performPaidSignCheck(mostExpensive, event); } } /** - * Checks if the given paid sign matches the given sign lines + * Gets the most specific paid sign match + * + *

This method chooses the most specific match, if any. If two matches have the same number of matched + * conditions, it will choose the most expensive one.

+ * + * @param allConditionMatches

A sorted list of all paid sign condition matches

+ * @return

The best matching paid sign, or null if no paid sign matches

+ */ + private PaidSign getMatchingSign(List allConditionMatches) { + PaidSign mostExpensive = null; + int bestMatch = 1; + for (PaidSignConditionMatch paidSignConditionMatch : allConditionMatches) { + int conditionMatches = paidSignConditionMatch.conditionMatches(); + /* Stop if there is no matching paid signs. Also stop if a paid sign is encountered with fewer conditions + matching */ + if (conditionMatches >= bestMatch) { + PaidSign paidSign = paidSignConditionMatch.paidSign(); + //Choose the most expensive paid sign between those with the same number of matched conditions + if (mostExpensive == null || paidSign.getCost() > mostExpensive.getCost()) { + mostExpensive = paidSign; + bestMatch = conditionMatches; + } + } else { + break; + } + } + return mostExpensive; + } + + /** + * Calculates the number of conditions matching for the given paid sign + * + *

This calculates the number of matches, allowing the most specific paid sign to be chosen

+ * + * @param paidSign

The paid sign to calculate matches for

+ * @param lines

The lines of a sign

+ * @param allConditionMatches

The number of conditions matched

+ */ + private void calculateConditionMatches(PaidSign paidSign, String[] lines, List allConditionMatches) { + if (paidSign.matches(lines)) { + if (paidSign.matchAnyCondition()) { + //For any match, it can only be assumed one lines matches + allConditionMatches.add(new PaidSignConditionMatch(paidSign, 1)); + } else { + //For a normal match, the number of conditions is equal to the number of matches + allConditionMatches.add(new PaidSignConditionMatch(paidSign, paidSign.getConditions().size())); + } + } else { + allConditionMatches.add(new PaidSignConditionMatch(paidSign, 0)); + } + } + + /** + * Performs necessary checks and performs the paid sign transaction * * @param paidSign

The paid sign to test against the sign lines

- * @param lines

The lines of a sign

* @param event

The triggered sign change event to cancel if necessary

- * @return

True if a match was found and actions have been taken

*/ - private boolean matchSign(PaidSign paidSign, String[] lines, SignChangeEvent event) { - if (paidSign.matches(lines)) { - Player player = event.getPlayer(); - String permission = paidSign.getPermission(); - //If a match is found, but the player is missing the permission, assume no plugin sign was created - if (permission != null && !permission.trim().isEmpty() && !player.hasPermission(permission)) { - return true; - } - - performPaidSignTransaction(paidSign, player, event); - return true; + private void performPaidSignCheck(PaidSign paidSign, SignChangeEvent event) { + Player player = event.getPlayer(); + String permission = paidSign.getPermission(); + //If a match is found, but the player is missing the permission, assume no plugin sign was created + if (permission != null && !permission.trim().isEmpty() && !player.hasPermission(permission)) { + return; } - return false; + + performPaidSignTransaction(paidSign, player, event); } /** diff --git a/src/main/java/net/knarcraft/paidsigns/manager/PaidSignManager.java b/src/main/java/net/knarcraft/paidsigns/manager/PaidSignManager.java index 0fe4086..164c9bb 100644 --- a/src/main/java/net/knarcraft/paidsigns/manager/PaidSignManager.java +++ b/src/main/java/net/knarcraft/paidsigns/manager/PaidSignManager.java @@ -156,7 +156,7 @@ public final class PaidSignManager { Map signConditions = sign.getConditions(); for (short lineIndex : signConditions.keySet()) { PaidSignCondition condition = signConditions.get(lineIndex); - conditionsSection.set(lineIndex + ".stringToMatch", condition.getStringToMatch()); + conditionsSection.set(lineIndex + ".stringToMatch", condition.stringToMatch()); conditionsSection.set(lineIndex + ".executeRegEx", condition.executeRegex()); conditionsSection.set(lineIndex + ".ignoreCase", condition.ignoreCase()); conditionsSection.set(lineIndex + ".ignoreColor", condition.ignoreColor()); diff --git a/src/main/java/net/knarcraft/paidsigns/manager/TrackedSignManager.java b/src/main/java/net/knarcraft/paidsigns/manager/TrackedSignManager.java index bc78b0f..0392404 100644 --- a/src/main/java/net/knarcraft/paidsigns/manager/TrackedSignManager.java +++ b/src/main/java/net/knarcraft/paidsigns/manager/TrackedSignManager.java @@ -126,8 +126,8 @@ public final class TrackedSignManager { 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()); + signSection.set(locationString + ".cost", sign.cost()); + signSection.set(locationString + ".playerId", sign.playerId().toString()); } configuration.save(signsFile); } @@ -142,8 +142,8 @@ public final class TrackedSignManager { if (!PaidSigns.getInstance().areRefundsEnabled() || !refund) { return; } - OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(trackedSign.getPlayerId()); - double refundSum = trackedSign.getCost() / 100 * PaidSigns.getInstance().getRefundPercentage(); + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(trackedSign.playerId()); + double refundSum = trackedSign.cost() / 100 * PaidSigns.getInstance().getRefundPercentage(); EconomyManager.deposit(offlinePlayer, refundSum); if (offlinePlayer instanceof Player player) { player.sendMessage(String.format(StringFormatter.replacePlaceholders(